"""Diagnostic Engine Core Module.

Provides the main diagnostic engine for automated problem detection,
analysis, and solution recommendation.
"""

import asyncio
import json
import logging
import time
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Any, Callable
from dataclasses import dataclass, field
from enum import Enum

logger = logging.getLogger(__name__)


class DiagnosticStatus(Enum):
    """Diagnostic session status."""
    INITIATED = "initiated"        # Session started
    DETECTING = "detecting"        # Running detection
    ANALYZING = "analyzing"        # Analyzing problems
    RECOMMENDING = "recommending"  # Generating recommendations
    APPLYING_FIX = "applying_fix"  # Applying automatic fixes
    COMPLETED = "completed"        # Diagnosis completed
    FAILED = "failed"             # Diagnosis failed
    CANCELLED = "cancelled"       # Session cancelled
    TIMEOUT = "timeout"           # Session timed out


class ProblemType(Enum):
    """Problem type classification."""
    NETWORK_CONNECTIVITY = "network_connectivity"
    HARDWARE_FAILURE = "hardware_failure"
    SOFTWARE_ERROR = "software_error"
    CONFIGURATION_ISSUE = "configuration_issue"
    PERFORMANCE_DEGRADATION = "performance_degradation"
    SECURITY_BREACH = "security_breach"
    RESOURCE_EXHAUSTION = "resource_exhaustion"
    SERVICE_UNAVAILABLE = "service_unavailable"
    DATA_CORRUPTION = "data_corruption"
    PROTOCOL_ERROR = "protocol_error"


class SeverityLevel(Enum):
    """Problem severity classification."""
    CRITICAL = "critical"     # System unusable
    HIGH = "high"            # Major functionality affected
    MEDIUM = "medium"        # Some functionality affected
    LOW = "low"              # Minor impact
    INFO = "info"            # Informational only


@dataclass
class DetectedProblem:
    """Represents a detected problem."""
    problem_id: str
    problem_type: ProblemType
    severity: SeverityLevel
    title: str
    description: str
    
    # Detection information
    detector_name: str
    detected_at: datetime
    confidence: float  # 0.0-1.0
    
    # Problem data
    symptom_data: Dict[str, Any]
    affected_components: List[str]
    error_codes: List[str] = field(default_factory=list)
    
    # Related information
    related_problems: List[str] = field(default_factory=list)
    historical_occurrences: int = 0
    
    # Resolution status
    is_resolved: bool = False
    resolution_method: Optional[str] = None


@dataclass
class AnalysisResult:
    """Result of problem analysis."""
    analysis_id: str
    analysis_type: str
    root_cause: Optional[str]
    contributing_factors: List[str] = field(default_factory=list)
    
    # Analysis metrics
    correlation_score: float
    pattern_matches: List[str] = field(default_factory=list)
    historical_similarity: float = 0.0
    
    # Conclusion
    conclusion: str
    confidence: float
    analysis_duration: float


@dataclass
class SolutionRecommendation:
    """Solution recommendation."""
    recommendation_id: str
    solution_type: str
    title: str
    description: str
    
    # Recommendation metrics
    confidence: float
    success_rate: float
    estimated_time: int  # seconds
    difficulty: str  # easy/medium/hard
    
    # Execution information
    is_automated: bool
    requires_approval: bool
    
    # Steps
    manual_steps: List[str] = field(default_factory=list)
    auto_fix_commands: List[Dict[str, Any]] = field(default_factory=list)
    
    # Risk information
    risk_level: str = "low"
    rollback_available: bool = False
    backup_required: bool = False
    
    # Related resources
    related_kb_articles: List[str] = field(default_factory=list)
    external_links: List[str] = field(default_factory=list)


@dataclass
class DiagnosticStep:
    """Individual diagnostic step."""
    step_id: str
    step_type: str  # detection, analysis, recommendation, fix
    description: str
    status: str  # pending, running, completed, failed
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    result_data: Dict[str, Any] = field(default_factory=dict)
    error_message: Optional[str] = None


@dataclass
class DiagnosticSession:
    """Diagnostic session context."""
    session_id: str
    device_id: str
    user_id: Optional[str]
    problem_description: str
    status: DiagnosticStatus
    created_at: datetime
    started_at: Optional[datetime] = None
    completed_at: Optional[datetime] = None
    
    # Diagnostic results
    diagnostic_steps: List[DiagnosticStep] = field(default_factory=list)
    detected_problems: List[DetectedProblem] = field(default_factory=list)
    analysis_results: List[AnalysisResult] = field(default_factory=list)
    recommendations: List[SolutionRecommendation] = field(default_factory=list)
    
    # Session metrics
    confidence_score: float = 0.0
    completion_rate: float = 0.0
    auto_fix_count: int = 0
    manual_step_count: int = 0
    
    # Metadata
    tags: List[str] = field(default_factory=list)
    priority: int = 5  # 1-10
    estimated_duration: int = 0  # seconds
    actual_duration: int = 0  # seconds


class DiagnosticEngine:
    """Main diagnostic engine class."""
    
    def __init__(self, config: Dict[str, Any]):
        """Initialize diagnostic engine.
        
        Args:
            config: Engine configuration
        """
        self.config = config
        self.engine_config = config.get("engine", {})
        
        # Engine settings
        self.max_concurrent_sessions = self.engine_config.get("max_concurrent_sessions", 10)
        self.session_timeout = self.engine_config.get("session_timeout", 300)
        self.detection_interval = self.engine_config.get("detection_interval", 30)
        self.confidence_threshold = self.engine_config.get("confidence_threshold", 0.6)
        self.auto_fix_threshold = self.engine_config.get("auto_fix_threshold", 0.8)
        self.max_history_days = self.engine_config.get("max_history_days", 90)
        
        # Active sessions
        self.active_sessions: Dict[str, DiagnosticSession] = {}
        self.session_tasks: Dict[str, asyncio.Task] = {}
        
        # Components (to be injected)
        self.problem_detectors: List[Any] = []
        self.analysis_engine: Optional[Any] = None
        self.knowledge_base: Optional[Any] = None
        self.report_generator: Optional[Any] = None
        
        # Session history and statistics
        self.session_history: List[DiagnosticSession] = []
        self.statistics = {
            "sessions_created": 0,
            "sessions_completed": 0,
            "sessions_failed": 0,
            "problems_detected": 0,
            "solutions_applied": 0,
            "auto_fixes_performed": 0
        }
        
        # Background tasks
        self.cleanup_task: Optional[asyncio.Task] = None
        
        logger.info(f"DiagnosticEngine initialized with {self.max_concurrent_sessions} max sessions")
    
    async def start(self):
        """Start diagnostic engine."""
        # Start cleanup task for expired sessions
        self.cleanup_task = asyncio.create_task(self._cleanup_expired_sessions())
        logger.info("DiagnosticEngine started")
    
    async def stop(self):
        """Stop diagnostic engine."""
        # Cancel all active sessions
        for session_id in list(self.active_sessions.keys()):
            await self.cancel_session(session_id, "Engine shutdown")
        
        # Cancel cleanup task
        if self.cleanup_task:
            self.cleanup_task.cancel()
            try:
                await self.cleanup_task
            except asyncio.CancelledError:
                pass
        
        logger.info("DiagnosticEngine stopped")
    
    def add_problem_detector(self, detector):
        """Add problem detector."""
        self.problem_detectors.append(detector)
        logger.info(f"Added problem detector: {detector.name}")
    
    def set_analysis_engine(self, engine):
        """Set analysis engine."""
        self.analysis_engine = engine
        logger.info("Analysis engine configured")
    
    def set_knowledge_base(self, kb):
        """Set knowledge base."""
        self.knowledge_base = kb
        logger.info("Knowledge base configured")
    
    def set_report_generator(self, generator):
        """Set report generator."""
        self.report_generator = generator
        logger.info("Report generator configured")
    
    async def start_diagnostic_session(self, device_id: str, 
                                     problem_description: str = "General diagnosis",
                                     user_id: Optional[str] = None,
                                     tags: Optional[List[str]] = None) -> str:
        """Start a new diagnostic session.
        
        Args:
            device_id: Target device ID
            problem_description: Description of the problem
            user_id: User who started the session
            tags: Optional tags for the session
            
        Returns:
            Session ID
            
        Raises:
            RuntimeError: If max concurrent sessions exceeded
        """
        if len(self.active_sessions) >= self.max_concurrent_sessions:
            raise RuntimeError(f"Maximum concurrent sessions ({self.max_concurrent_sessions}) exceeded")
        
        # Create new session
        session_id = str(uuid.uuid4())
        session = DiagnosticSession(
            session_id=session_id,
            device_id=device_id,
            user_id=user_id,
            problem_description=problem_description,
            status=DiagnosticStatus.INITIATED,
            created_at=datetime.now(),
            tags=tags or []
        )
        
        # Add to active sessions
        self.active_sessions[session_id] = session
        
        # Start diagnostic task
        task = asyncio.create_task(self._run_diagnostic_session(session))
        self.session_tasks[session_id] = task
        
        # Update statistics
        self.statistics["sessions_created"] += 1
        
        logger.info(f"Started diagnostic session {session_id} for device {device_id}")
        return session_id
    
    async def get_session_status(self, session_id: str) -> Optional[DiagnosticSession]:
        """Get diagnostic session status.
        
        Args:
            session_id: Session ID
            
        Returns:
            Session object or None if not found
        """
        return self.active_sessions.get(session_id)
    
    async def cancel_session(self, session_id: str, reason: str = "Cancelled by user"):
        """Cancel diagnostic session.
        
        Args:
            session_id: Session ID to cancel
            reason: Cancellation reason
        """
        if session_id not in self.active_sessions:
            return False
        
        # Cancel session task
        if session_id in self.session_tasks:
            task = self.session_tasks[session_id]
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
            del self.session_tasks[session_id]
        
        # Update session status
        session = self.active_sessions[session_id]
        session.status = DiagnosticStatus.CANCELLED
        session.completed_at = datetime.now()
        session.actual_duration = int((session.completed_at - session.created_at).total_seconds())
        
        # Move to history
        self.session_history.append(session)
        del self.active_sessions[session_id]
        
        logger.info(f"Cancelled diagnostic session {session_id}: {reason}")
        return True
    
    async def apply_recommendation(self, session_id: str, 
                                 recommendation_id: str) -> bool:
        """Apply a solution recommendation.
        
        Args:
            session_id: Session ID
            recommendation_id: Recommendation ID to apply
            
        Returns:
            True if applied successfully
        """
        session = self.active_sessions.get(session_id)
        if not session:
            return False
        
        # Find recommendation
        recommendation = None
        for rec in session.recommendations:
            if rec.recommendation_id == recommendation_id:
                recommendation = rec
                break
        
        if not recommendation:
            logger.warning(f"Recommendation {recommendation_id} not found in session {session_id}")
            return False
        
        try:
            if recommendation.is_automated:
                # Apply automated fix
                success = await self._apply_automated_fix(recommendation)
                if success:
                    session.auto_fix_count += 1
                    self.statistics["auto_fixes_performed"] += 1
                    logger.info(f"Applied automated fix: {recommendation.title}")
                return success
            else:
                # Manual fix - just log and return success
                session.manual_step_count += len(recommendation.manual_steps)
                logger.info(f"Manual fix instructions provided: {recommendation.title}")
                return True
                
        except Exception as e:
            logger.error(f"Error applying recommendation {recommendation_id}: {e}")
            return False
    
    async def get_session_report(self, session_id: str, 
                               format: str = "json") -> Optional[bytes]:
        """Generate session diagnostic report.
        
        Args:
            session_id: Session ID
            format: Report format (json, html, pdf)
            
        Returns:
            Report data or None if session not found
        """
        session = self.active_sessions.get(session_id)
        if not session and session_id not in [s.session_id for s in self.session_history]:
            return None
        
        if not session:
            # Find in history
            for hist_session in self.session_history:
                if hist_session.session_id == session_id:
                    session = hist_session
                    break
        
        if not self.report_generator:
            # Generate basic JSON report
            report_data = {
                "session_id": session.session_id,
                "device_id": session.device_id,
                "status": session.status.value,
                "created_at": session.created_at.isoformat(),
                "problems": [
                    {
                        "id": p.problem_id,
                        "type": p.problem_type.value,
                        "severity": p.severity.value,
                        "title": p.title,
                        "description": p.description,
                        "confidence": p.confidence
                    }
                    for p in session.detected_problems
                ],
                "recommendations": [
                    {
                        "id": r.recommendation_id,
                        "title": r.title,
                        "description": r.description,
                        "confidence": r.confidence,
                        "automated": r.is_automated
                    }
                    for r in session.recommendations
                ],
                "statistics": {
                    "problems_detected": len(session.detected_problems),
                    "recommendations_generated": len(session.recommendations),
                    "confidence_score": session.confidence_score,
                    "duration": session.actual_duration
                }
            }
            return json.dumps(report_data, indent=2).encode('utf-8')
        else:
            # Use report generator
            return await self.report_generator.generate_report(session, format)
    
    def get_engine_statistics(self) -> Dict[str, Any]:
        """Get engine statistics."""
        return {
            "engine_status": {
                "active_sessions": len(self.active_sessions),
                "max_concurrent_sessions": self.max_concurrent_sessions,
                "total_detectors": len(self.problem_detectors),
                "has_analysis_engine": self.analysis_engine is not None,
                "has_knowledge_base": self.knowledge_base is not None
            },
            "session_statistics": self.statistics.copy(),
            "detector_statistics": [
                {
                    "name": detector.name,
                    "enabled": detector.enabled,
                    "last_check": detector.last_check_time.isoformat() if detector.last_check_time else None
                }
                for detector in self.problem_detectors
            ]
        }
    
    async def _run_diagnostic_session(self, session: DiagnosticSession):
        """Run diagnostic session workflow."""
        try:
            session.started_at = datetime.now()
            
            # Step 1: Problem Detection
            await self._run_detection_step(session)
            
            # Step 2: Problem Analysis
            if session.detected_problems and self.analysis_engine:
                await self._run_analysis_step(session)
            
            # Step 3: Solution Recommendation
            if self.knowledge_base:
                await self._run_recommendation_step(session)
            
            # Step 4: Calculate final metrics
            await self._calculate_session_metrics(session)
            
            # Session completed successfully
            session.status = DiagnosticStatus.COMPLETED
            session.completed_at = datetime.now()
            session.actual_duration = int((session.completed_at - session.created_at).total_seconds())
            
            self.statistics["sessions_completed"] += 1
            
            logger.info(f"Diagnostic session {session.session_id} completed successfully")
            
        except asyncio.CancelledError:
            # Session was cancelled
            session.status = DiagnosticStatus.CANCELLED
            raise
            
        except Exception as e:
            # Session failed
            session.status = DiagnosticStatus.FAILED
            session.completed_at = datetime.now()
            session.actual_duration = int((session.completed_at - session.created_at).total_seconds())
            
            self.statistics["sessions_failed"] += 1
            
            logger.error(f"Diagnostic session {session.session_id} failed: {e}")
            
        finally:
            # Move session to history and cleanup
            if session.session_id in self.active_sessions:
                self.session_history.append(session)
                del self.active_sessions[session.session_id]
            
            if session.session_id in self.session_tasks:
                del self.session_tasks[session.session_id]
    
    async def _run_detection_step(self, session: DiagnosticSession):
        """Run problem detection step."""
        session.status = DiagnosticStatus.DETECTING
        
        step = DiagnosticStep(
            step_id=f"detection_{int(time.time())}",
            step_type="detection",
            description="Running problem detectors",
            status="running",
            started_at=datetime.now()
        )
        session.diagnostic_steps.append(step)
        
        detected_problems = []
        device_context = {"device_id": session.device_id}
        
        # Run all enabled detectors
        for detector in self.problem_detectors:
            if not detector.enabled:
                continue
                
            try:
                detector_problems = await detector.detect_problems(device_context)
                for problem in detector_problems:
                    if problem.confidence >= self.confidence_threshold:
                        detected_problems.append(problem)
                        
            except Exception as e:
                logger.error(f"Error running detector {detector.name}: {e}")
        
        # Filter and deduplicate problems
        session.detected_problems = self._deduplicate_problems(detected_problems)
        
        step.status = "completed"
        step.completed_at = datetime.now()
        step.result_data = {
            "problems_detected": len(session.detected_problems),
            "detectors_run": len([d for d in self.problem_detectors if d.enabled])
        }
        
        self.statistics["problems_detected"] += len(session.detected_problems)
        
        logger.info(f"Detection completed: {len(session.detected_problems)} problems found")
    
    async def _run_analysis_step(self, session: DiagnosticSession):
        """Run problem analysis step."""
        session.status = DiagnosticStatus.ANALYZING
        
        step = DiagnosticStep(
            step_id=f"analysis_{int(time.time())}",
            step_type="analysis",
            description="Analyzing detected problems",
            status="running",
            started_at=datetime.now()
        )
        session.diagnostic_steps.append(step)
        
        try:
            device_context = {"device_id": session.device_id}
            analysis_results = await self.analysis_engine.analyze_problems(
                session.detected_problems, device_context
            )
            session.analysis_results = analysis_results
            
            step.status = "completed"
            step.result_data = {
                "analyses_completed": len(analysis_results),
                "correlations_found": len([a for a in analysis_results if "correlation" in a.analysis_type])
            }
            
        except Exception as e:
            step.status = "failed"
            step.error_message = str(e)
            logger.error(f"Analysis step failed: {e}")
        
        step.completed_at = datetime.now()
        session.diagnostic_steps.append(step)
    
    async def _run_recommendation_step(self, session: DiagnosticSession):
        """Run solution recommendation step."""
        session.status = DiagnosticStatus.RECOMMENDING
        
        step = DiagnosticStep(
            step_id=f"recommendation_{int(time.time())}",
            step_type="recommendation",
            description="Generating solution recommendations",
            status="running",
            started_at=datetime.now()
        )
        session.diagnostic_steps.append(step)
        
        try:
            recommendations = await self.knowledge_base.get_recommendations(
                session.detected_problems, session.analysis_results
            )
            session.recommendations = recommendations
            
            step.status = "completed"
            step.result_data = {
                "recommendations_generated": len(recommendations),
                "automated_solutions": len([r for r in recommendations if r.is_automated])
            }
            
        except Exception as e:
            step.status = "failed"
            step.error_message = str(e)
            logger.error(f"Recommendation step failed: {e}")
        
        step.completed_at = datetime.now()
    
    async def _calculate_session_metrics(self, session: DiagnosticSession):
        """Calculate session metrics."""
        if session.detected_problems:
            # Calculate overall confidence score
            total_confidence = sum(p.confidence for p in session.detected_problems)
            session.confidence_score = total_confidence / len(session.detected_problems)
            
            # Calculate completion rate based on completed steps
            completed_steps = len([s for s in session.diagnostic_steps if s.status == "completed"])
            total_steps = len(session.diagnostic_steps)
            session.completion_rate = completed_steps / total_steps if total_steps > 0 else 0.0
        
        # Count manual steps in recommendations
        session.manual_step_count = sum(len(r.manual_steps) for r in session.recommendations)
    
    async def _apply_automated_fix(self, recommendation: SolutionRecommendation) -> bool:
        """Apply automated fix recommendation."""
        try:
            for command in recommendation.auto_fix_commands:
                action = command.get("action")
                params = command.get("params", [])
                
                if action == "wait":
                    # Wait for specified seconds
                    await asyncio.sleep(params[0] if params else 1)
                    
                elif action == "systemctl":
                    # Simulate systemctl command (would use subprocess in real implementation)
                    logger.info(f"Simulated systemctl command: {' '.join(params)}")
                    
                elif action == "test_connectivity":
                    # Test network connectivity
                    logger.info("Testing connectivity after fix")
                    
                else:
                    logger.warning(f"Unknown auto-fix action: {action}")
            
            return True
            
        except Exception as e:
            logger.error(f"Error applying automated fix: {e}")
            return False
    
    def _deduplicate_problems(self, problems: List[DetectedProblem]) -> List[DetectedProblem]:
        """Deduplicate similar problems."""
        # Simple deduplication based on problem type and affected components
        unique_problems = []
        seen_signatures = set()
        
        for problem in problems:
            # Create signature for deduplication
            signature = (
                problem.problem_type.value,
                tuple(sorted(problem.affected_components)),
                problem.title
            )
            
            if signature not in seen_signatures:
                unique_problems.append(problem)
                seen_signatures.add(signature)
            else:
                # Update existing problem confidence if higher
                for existing in unique_problems:
                    existing_sig = (
                        existing.problem_type.value,
                        tuple(sorted(existing.affected_components)),
                        existing.title
                    )
                    if existing_sig == signature and problem.confidence > existing.confidence:
                        existing.confidence = problem.confidence
                        break
        
        return sorted(unique_problems, key=lambda p: p.confidence, reverse=True)
    
    async def _cleanup_expired_sessions(self):
        """Background task to cleanup expired sessions."""
        while True:
            try:
                await asyncio.sleep(60)  # Check every minute
                
                current_time = datetime.now()
                expired_sessions = []
                
                for session_id, session in self.active_sessions.items():
                    session_age = (current_time - session.created_at).total_seconds()
                    if session_age > self.session_timeout:
                        expired_sessions.append(session_id)
                
                # Cancel expired sessions
                for session_id in expired_sessions:
                    await self.cancel_session(session_id, "Session timeout")
                
                # Cleanup old history entries
                cutoff_date = current_time - timedelta(days=self.max_history_days)
                self.session_history = [
                    s for s in self.session_history 
                    if s.created_at > cutoff_date
                ]
                
            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in session cleanup: {e}")


# Example usage and testing
if __name__ == "__main__":
    import asyncio
    
    async def test_diagnostic_engine():
        """Test diagnostic engine."""
        # Create configuration
        config = {
            "engine": {
                "max_concurrent_sessions": 5,
                "session_timeout": 300,
                "confidence_threshold": 0.6,
                "auto_fix_threshold": 0.8
            }
        }
        
        # Create and start engine
        engine = DiagnosticEngine(config)
        await engine.start()
        
        try:
            # Start diagnostic session
            session_id = await engine.start_diagnostic_session(
                device_id="test_device_001",
                problem_description="Network connectivity issues",
                user_id="test_user"
            )
            
            print(f"Started session: {session_id}")
            
            # Wait for diagnosis to complete
            await asyncio.sleep(2)
            
            # Get session status
            session = await engine.get_session_status(session_id)
            if session:
                print(f"Session status: {session.status.value}")
                print(f"Problems detected: {len(session.detected_problems)}")
                print(f"Recommendations: {len(session.recommendations)}")
            
            # Get session report
            report = await engine.get_session_report(session_id, "json")
            if report:
                print(f"Report generated: {len(report)} bytes")
            
            # Get engine statistics
            stats = engine.get_engine_statistics()
            print(f"Engine statistics: {stats}")
            
        finally:
            await engine.stop()
    
    # Run test
    asyncio.run(test_diagnostic_engine())